Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

220 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dashboard

Дашборд патчей: для одного или нескольких koji-тегов собирает последние билды (с учётом наследования тегов), по extra.source.original_url определяет ветку GitLab, с которой собран каждый билд, читает в этой ветке каталог PATCH, классифицирует найденные файлы (AUTOGEN / CVE / SAST / DAST / COVERAGE / DISTSUFFIX / LICENSE / SPEC / CHANGELOG / FILES / other) и показывает всё это в HTML-дашборде с вкладкой «Состояние» по каждому тегу и вкладкой «Изменения», сравнивающей теги между собой.

Данные и представление в проекте разделены, и делают их разные команды. collect ходит в koji и GitLab и кладёт снапшот тега в JSON — это данные, и только они. page кладёт на диск страницу, внутри которой нет ни одного снапшота, — это представление, и только оно. Снапшоты в страницу подгружает человек, уже открыв её в браузере.

Разделение не ради стройности: снапшоты, которые захочется сравнить, обычно собраны в разные дни и лежат в разных файлах, а какие именно файлы человек положит рядом, в момент сборки страницы не знает никто. Готовым такой дифф взяться неоткуда — считать его некому, кроме самой страницы. Поэтому дашборд считает и дифф, и счётчики, и порядок строк сам, из того, что в него подгрузили.

Страница — один HTML-файл без внешних зависимостей: CSS и JS встроены в неё, сеть ей не нужна, работает при открытии прямо с диска. Пересобирать её при появлении новых снапшотов не надо — она от данных не зависит.

Требования

  • Python 3.9+
  • PyYAML — разбор конфига; нужен всегда, модуль конфигурации импортируется при любом запуске
  • koji — клиент XML-RPC к хабу; нужен только для collect, импортируется внутри самой команды
  • requests — HTTP-запросы к GitLab REST v4; нужен только для collect, импортируется лениво внутри транспорта, который дёргает сеть только при сборе

page не требует ни koji, ни requests, ни даже конфига: страница пуста, пока в неё не подгрузят снапшоты, и ходить ей некуда.

Все модули должны быть доступны в интерпретаторе, которым запускается дашборд; отдельного requirements.txt в проекте нет. Поставить недостающее можно, например, через pip install koji requests pyyaml — пакет koji на PyPI и есть официальный клиент проекта Koji.

Установка

Дашборд запускается двумя способами, и оба равноправны.

Из каталога с исходниками, без установки вовсе:

python3 -m dashboard --version

Или пакетом, тогда появляется команда dashboard:

pip install --no-build-isolation -e .
dashboard --version

--no-build-isolation здесь не прихоть: без него pip уходит в сеть за свежим setuptools, а на машине сборки её может не быть. Метаданные пакета лежат в setup.cfg, а не в [project] внутри pyproject.toml: [project] понимают только setuptools от 61-й версии, тогда как в RHEL 9 стоит 53-я.

Зависимости в метаданных не перечислены нарочно: koji, requests и PyYAML ставят системным пакетным менеджером, где они собраны под тот же питон, что и сам koji-клиент. Перечисли мы их здесь — pip дрался бы с дистрибутивом за одни и те же файлы.

Дальше в README везде написано python3 -m dashboard; если пакет установлен, вместо этого работает просто dashboard.

Быстрый старт

export GITLAB_TOKEN=glpat-...                 # опц., см. «Токен GitLab»
cp dashboard.example.yaml dashboard.yaml      # поправить адреса под себя
python3 -m dashboard --config dashboard.yaml collect \
    --tag os-9.2 -o os-9.2.json
python3 -m dashboard page -o dashboard.html

Дальше dashboard.html открывают в браузере и перетаскивают в него os-9.2.json. С одним снапшотом работает вкладка «Состояние».

Сравнить два тега — собрать оба снапшота:

python3 -m dashboard --config dashboard.yaml collect \
    --tag os-9.1 --tag os-9.2 -o snapshots.json

Несколько --tag пишут в один файл список снапшотов, так что подгрузить его можно одним файлом. Как только на странице оказалось два снапшота, появляется вкладка «Изменения».

Сравнить тег с ним же месяц назад — те же два снапшота, только собранные в разное время:

python3 -m dashboard --config dashboard.yaml collect --tag os-9.2 \
    -o os-9.2-07-01.json
# ... через месяц ...
python3 -m dashboard --config dashboard.yaml collect --tag os-9.2 \
    -o os-9.2-08-01.json

Оба файла подгружаются в ту же самую страницу: снапшот опознаётся парой «тег и время сбора», поэтому два прогона одного тега страница различает и сравнивает между собой.

Посмотреть дашборд, не имея доступа к koji, можно на снапшотах из тестов. Это обычные файлы снапшотов, они подгружаются так же, как собранные вами:

tests/fixtures/rich-old.json     os-9.1
tests/fixtures/rich-new.json     os-9.2
tests/fixtures/rich-newer.json   os-9.3
tests/fixtures/rich-newest.json  os-9.4
tests/fixtures/rich-again.json   os-9.4, собранный месяцем позже
tests/fixtures/rich-mirror.json  os-9.5
tests/fixtures/rich-wide.json    os-9.6
tests/fixtures/rich-many.json    os-9.7

Берите все восемь: каждый следующий показывает то, чего не показывают предыдущие. В них нарочно собрано то, ради чего дашборд и написан — все классы патчей, унаследованные и затегованные прямо билды, компонент с неизвестным тегом, подпакеты по четырём архитектурам, ошибка GitLab и внутренняя ошибка.

На двух снапшотах не видно ни сводной пары, ни рельса из трёх узлов. Третий добавляет случаи, которые на двух не показать: vim уходил в os-9.2 и вернулся тем же билдом, zlib откатывался и поднялся обратно на прежний релиз. По шагам оба двигались, а сводная пара os-9.1 → os-9.3 скажет, что ничего не изменилось, — за этим она и нужна.

Четвёртый добавляет своё. Патчи класса DISTSUFFIX — в первых трёх их нет вовсе, а один из них нарочно назван kernel.spec.distsuffix.patch, чтобы было видно: файл со «спековым» именем всё равно уходит в DISTSUFFIX. Диапазон, который не сводный и не соседний, — на трёх узлах такого нет вовсе, там os-9.1 → os-9.3 и есть вся цепочка; здесь curl, появившийся в os-9.2 и исчезнувший в os-9.3, возвращается тем же билдом, и os-9.2 → os-9.4 честно говорит «не изменилось», пока сводный os-9.1 → os-9.4 говорит «появился». И время: os-9.4 собран через восемь часов после os-9.3, а не через месяц, — на рельсе видно, что расстояние между узлами меряется той единицей, которая ему подходит.

Пятый — тот же тег os-9.4, собранный месяцем позже. Снапшот опознаётся парой «тег и время сбора», и на рельсе это два узла с одним именем: пара из них отвечает на вопрос «что за месяц случилось с тегом», а не «чем один тег отличается от другого». Внутри — httpd, пересобранный другим владельцем из переехавшей в другую группу GitLab ветки: ни владелец, ни проект своей метки не имеют, их показывает раскрытая строка. Там же zlib, пересобранный руками из готового SRPM: ветки у такого билда нет, каталог PATCH читать негде, и пара «ветка → srpm» в раскрытии «Изменений» показана как есть. И openssl без владельца и без времени сборки — колонке «владелец» тоже есть что показать пустой.

Шестой собран с другого koji-хаба, и страница об этом предупреждает окошком: сравнивать билды разных хабов обычно бессмысленно, но бывает и наоборот — переезд, зеркало, — поэтому снапшот принимается, а не отвергается. Заодно видно, что ссылка на билд ведёт на хаб того снапшота, из которого билд приехал: в паре os-9.4 → os-9.5 левая сторона ведёт на прежний хаб, правая — на зеркало.

Седьмой — про то, обо что таблица разъезжается. У chromium в нём патчи всех классов разом, обе ошибки, унаследованный тег и сборка с коммита: полтора десятка меток в одной ячейке, сорокасимвольный хеш, длинный путь проекта и два десятка подпакетов по четырём архитектурам. В живом теге такой билд один на сотню, и попадается он позже, чем правят вёрстку.

Восьмой — тег величиной с настоящий, сто пять билдов. Сортировка меняет порядок сотни строк, поиск отсекает, а не подсвечивает одну, числа на карточках и в меню фильтров перестают быть однозначными, «развернуть все» разворачивает сотню карточек. Патчи, проблемы, наследование, сборка с коммита и отсутствие исходников расставлены по кругу, чтобы попадаться вперемешку.

Класс DISTSUFFIX встаёт в карточках последним, за other, и это не сбой: список классов страница складывает из всех загруженных снапшотов, а в первых трёх его нет — их patch_classes оставлен прежним нарочно, из них порождён эталон page-data.golden.json, пересобрать который уже нечем. Так же выглядела бы страница, на которую положили снапшоты, собранные разными версиями dashboard: класс, известный не всем, дописывается в конец. Тем же хвостом встаёт и LICENSE — он появился ещё позже, и его знают только четыре последних снапшота.

Рядом лежат snapshot-os-9.1.json и snapshot-os-9.2.json: они старого формата, без patch_classes и тегов билдов, и годятся разве что затем, чтобы посмотреть, как страница читает снапшот прежней версии.

--config можно не передавать флагом, а задать переменной окружения DASHBOARD_CONFIG.

Подкоманды

collect — собрать снапшоты в JSON

python3 -m dashboard --config dashboard.yaml collect \
    --tag os-9.1 --tag os-9.2 -o snapshots.json

--tag можно указывать несколько раз — тогда в выходной файл будет записан список снапшотов, по одному на тег. Требует koji.hub (в конфиге или через --koji-hub). Без -o пишет snapshot.json.

page — положить страницу на диск

python3 -m dashboard page -o dashboard.html

Собирает шаблон и скрипты в один самодостаточный файл и записывает его. Данных внутри нет, поэтому ни koji, ни GitLab, ни конфиг команде не нужны: пример выше отработал без --config. Без -o пишет dashboard.html.

Пересобирать страницу нужно только после обновления самого dashboard — новые снапшоты в неё просто подгружают.

Общие флаги (перед именем подкоманды)

Флаг Что делает
--config PATH путь к YAML-конфигу (или DASHBOARD_CONFIG в окружении)
--koji-hub URL перекрыть koji.hub из конфига
--gitlab-api URL перекрыть адрес GitLab API (пишется как хост default_host или *, если default_host не задан)
--patch-dir NAME перекрыть имя каталога патчей (по умолчанию PATCH)
--jobs N параллельных запросов к GitLab при сборе, по умолчанию 8
--max-problems N см. «Коды возврата» — учитывается только у collect
--log-level {error,warning,info,debug} подробность лога, по умолчанию info; см. «Логи»
--version напечатать версию и выйти; подкоманда при этом не нужна

Эти флаги общие для всего CLI и должны идти до имени подкоманды (dashboard --log-level debug collect --tag ..., а не dashboard collect --log-level debug --tag ...) — таковы правила argparse-подпарсеров, используемых в проекте.

Конфигурация

Конфиг — YAML-файл, пример в dashboard.example.yaml. Все ключи, кроме koji.hub (для collect) и gitlab.hosts.<host>.api (для каждого описанного хоста), необязательны.

Ключ Обязателен По умолчанию Что делает
koji.hub да для collect, не нужен для page XML-RPC адрес хаба; можно перекрыть --koji-hub
koji.web нет нет ссылок на koji в дашборде база для ссылок вида /search?match=exact&type=build&terms=NVR
gitlab.default_host нет хост, на который будет отправлен запрос, если хост из original_url не описан в gitlab.hosts
gitlab.token_env нет GITLAB_TOKEN имя переменной окружения, из которой читается токен GitLab
gitlab.hosts.<host>.api да для каждого описанного хоста база REST v4, например https://gitlab.example.com/api/v4
gitlab.hosts.<host>.web нет api до /api/... база для веб-ссылок на дерево и файлы репозитория
patch_dir нет PATCH имя каталога патчей в корне репозитория; можно перекрыть --patch-dir
patch_classes нет правила для AUTOGEN, CVE, SAST, DAST, COVERAGE, DISTSUFFIX, LICENSE, SPEC, CHANGELOG, FILES + other: '.*' список правил {name, pattern} — регулярка по имени файла патча → класс

Правила patch_classes применяются по порядку, побеждает первое совпадение (re.search, без привязки к началу строки). Первым идёт AUTOGEN — имена, начинающиеся с autogen- или autogen_ (autogen-sast-patches.inc.new). Это сгенерированные перечни патчей, а не патчи, и стоять правило обязано именно первым: в самих этих именах есть sast, cve и fuzz, так что ниже по списку файлы уехали бы в SAST и DAST и накрутили их счётчики. Привязка к началу имени тоже не случайна — httpd-autogen-fix.patch.new это обычный патч.

Дальше CVE опознаётся по идентификатору в любом месте имени (httpd-2.4.62-cve-2024-42516.patch.new), SAST, DAST и COVERAGE — по вхождению маркера, тоже в любом месте (SAST-src.core.ngx_file.c.patch.new и httpd-2.4.62-sast-src.core.c.patch.new оба попадут в SAST, COVERAGE-parser.patch.new — в COVERAGE). В DAST кроме dast попадает и fuzz (FUZZ-parser.patch.new, httpd-2.4.62-fuzz-parser.patch.new): фаззинг — то же динамическое тестирование, и отдельной категорией он бы только дробил отчёт. DISTSUFFIX — по distsuffix.patch (nginx-distsuffix.patch, kernel.spec.distsuffix.patch): такой патч переклеивает суффикс сборки, и правило стоит выше SPEC — правит он и правда спек, но класс отвечает на вопрос «зачем патч». Расширение в правиле обязательно, иначе в класс уехал бы и distsuffix.inc. LICENSE — по слову license в любом месте имени (nginx-license.patch, LICENSE.txt, британское licence тоже): такой патч обычно правит поле License в спеке, и правило стоит выше SPEC по той же причине, что и предыдущее. Расширения оно не требует — файл с этим словом в каталоге патчей про лицензию и есть, чем бы он ни был. SPEC опознаётся по .spec. с точками (nginx.spec.patch, httpd-2.4.62.spec.patch.new), CHANGELOG — по changelog.yaml (короткое .yml тоже ловится), а FILES — по .tar.gz. Точки в правиле SPEC обязательны: без них в класс попали бы specialcase.patch и respec-fix.patch.

Имя, где есть и CVE-идентификатор, и маркер SAST, уйдёт в CVE: правило CVE стоит выше. По той же причине httpd-cve-2024-42516.spec.patch — это CVE, а не SPEC, а cve-2024-42516-sources.tar.gz — CVE, а не FILES: класс отвечает на вопрос «зачем файл», а не «какого он вида». Расширение роли не играет — классифицируется всё имя файла целиком, так что .patch, .patch.new и любое другое равнозначны.

FILES — это не патчи, а просто файлы, лежащие в том же каталоге. Категория задумана расширяемой: когда встретится ещё одно расширение, дописывайте его в то же правило альтернативой ('(?i)\.(?:tar\.gz|zip|bin)'), а не заводите новый класс. Цветов, которые человек различает в тонкой полоске состава патчей, конечное число, и восемь классов — это уже предел; каждый следующий отбирает различимость у остальных. Если понадобится строже, чтобы маркер не ловился внутри слова, замените правило на '(?i)(?:^|[^a-z0-9])sast(?:[^a-z0-9]|$)'. Если последнее правило не всеохватное, к списку автоматически добавляется other: '.*', так что любой файл в каталоге патчей всегда получает класс. Разделы koji, gitlab и gitlab.hosts.<host> при разборе проверяются на форму: если вместо отображения ({...}) там оказалась строка или список, load_config поднимает ConfigError с понятным сообщением, а не падает трейсбеком.

Имена классов в порядке правил уезжают в снапшот полем patch_classes — у дашборда конфига нет, а порядок карточек и меток он берёт именно оттуда (см. «Формат снапшота»).

Токен GitLab

Токен читается только из переменной окружения (имя задаётся gitlab.token_env, по умолчанию GITLAB_TOKEN) — специального флага для него нет, чтобы он не попадал в историю шелла и в список процессов:

export GITLAB_TOKEN=glpat-...

Токен опционален. Без него запросы к GitLab идут анонимно — рабочий режим для публичных репозиториев; для приватных 401/403 в ответ на запрос дерева PATCH не роняют весь прогон, а записываются в problems конкретного билда (см. «Формат снапшота»), и такой билд помечается в дашборде тегом gitlab-error.

Дашборд

Как в него попадают снапшоты

Свежая страница показывает только зону загрузки: вкладки без данных обещали бы содержимое, которого нет. Файлы снапшотов в неё либо перетаскивают (ронять можно на всё окно, не только на зону), либо выбирают кнопкой; и то, и другое принимает сразу несколько файлов. Третьего способа нет намеренно: страницу открывают с диска, а браузер запрещает ей читать соседние файлы по адресу, так что дверей ровно две — те, где файл даёт сам человек.

Когда снапшоты уже есть, добавляют их там же, где показаны: в конце рельса стоит пунктирный «+ добавить», открывающий тот же диалог. Ронять файлы на страницу можно по-прежнему.

Снапшоты выстраиваются в цепочку по времени сбора, от раннего к позднему: именно в этом порядке они сравниваются попарно, и он же виден на рельсе над вкладками. Рельс — главный орган страницы: узел на нём это снапшот, под тегом стоит время сбора, а над отрезком между соседними узлами — расстояние между ними во времени («7 ч», «31 дн», «2 мес»). Единица крупная нарочно: точные даты и так стоят под узлами, а от подписи нужен порядок величины. Длинная цепочка рельс не переносит, а прокручивает: перенос оставил бы на конце строки отрезок, ведущий в пустоту. Прокручивают колесом мыши, наведя курсор на рельс, — полосы прокрутки под ним нет: рельс сам линия, и вторая линия под ней читалась бы как часть рисунка. Докрученная до конца цепочка колесо странице не отдаёт: чтобы уехать со страницей, курсор с рельса отводят — полоса у него узкая. Отрезок при этом не ужимается уже своей подписи: длинная цепочка уезжает за край, но «31 дн» над отрезком не наползает на соседние узлы.

Составом управляют на самом рельсе. Порядок меняют перетаскиванием узла: пока узел едет, сосед, к которому его подносят, показывает полосой, с какой стороны тот встанет. Состоявшаяся перестановка отключает автоматическую сортировку до конца жизни страницы: раз человек сказал, в каком порядке сравнивать, дальше подгруженные файлы дописываются в конец цепочки и порядок больше никто не трогает. Убирает снапшот крестик в углу узла — он проявляется при наведении и при фокусе. Убрать можно и последний снапшот: страница тогда возвращается к зоне загрузки. Число билдов и имя файла, из которого снапшот приехал, стоят в подсказке узла.

Один и тот же снапшот дважды не загрузится: снапшот опознаётся парой «тег и время сбора», и повторная попытка отклоняется с сообщением, а не удваивает цепочку. Из той же пары следует, что два прогона одного тега — законный случай: они различимы, и на рельсе их различает время сбора, стоящее под каждым тегом.

Отказы называются по имени файла и всплывают окошком в правом нижнем углу: не разбирается как JSON, чужая версия схемы, «это не снапшот dashboard», файл не читается. Одно окошко на всю пачку файлов, внутри — по строке на причину; длинный список режется, и последняя строка говорит, сколько причин скрыто. Окошко гаснет через десять секунд само, но под курсором отсчёт стоит, а крестик убирает его сразу. Больше четырёх окошек на экране не собирается: пятое выталкивает самое старое.

Сообщение лежит поверх страницы и ничего в ней не двигает — строка в потоке увозила бы таблицу сначала вниз, а через десять секунд обратно.

Отдельный случай — данные, на которых страница не рисуется: загрузка тогда откатывается целиком, состав возвращается к прежнему, а причина встаёт рядом с именем файла. Перестановка и удаление откатываются по тому же правилу, только причина уезжает в предупреждения — своего места рядом с файлом у них нет. Разные koji_hub в снапшотах — не отказ, а предупреждение: сравнивать такие снапшоты обычно бессмысленно, но переезд хаба и зеркала бывают. Предупреждения всплывают тем же окошком и показываются один раз: перестановка снапшотов их не повторяет — состав от неё не меняется.

Загруженное живёт только до перезагрузки страницы. Никакого хранилища у дашборда нет — он ничего не пишет ни в браузер, ни на диск, и F5 возвращает пустую зону загрузки. Это цена того, что страница ничего о себе не помнит и файлом её можно передать кому угодно, ничего с собой не унося.

Вкладка «Состояние»

Отвечает на вопрос «что сейчас в теге»: карточки-счётчики (всего билдов, с патчами, унаследованных, проблемных, отдельно по каждому классу патчей) и таблица билдов с раскрытием строки — ссылки на Koji и GitLab, патчи по классам (со ссылками на файл), пакеты блоками по архитектуре, проблемы. Снапшот выбирают на рельсе над вкладками: клик по узлу открывает его, открытый узел залит и подсвечен. Отдельного переключателя нет — число билдов и имя файла каждого снапшота стоят в списке источников. При единственном снапшоте узел не нажимается: переключать не на что. Открывается вкладка на последнем снапшоте цепочки — при обычном порядке это самый свежий из загруженных.

Колонка «тег» показывает, откуда билд взялся: прочерк — билд затегован прямо в выбранный тег, имя — унаследован из этого родительского тега. Знак вопроса означает, что снапшот собран версией, которая тег ещё не записывала; «неизвестно» и «прямой» намеренно не смешиваются.

Раскрытая строка держится за свою: полоса у левого края идёт через строку и её детали насквозь, поэтому при нескольких раскрытых строках сразу видно, где чьи блоки. У билда с проблемой полоса красная — та же, что метит строку и без раскрытия.

Путь патча стоит второй строкой только тогда, когда он что-то добавляет к имени. Обычно путь — это PATCH/<имя>, то есть имя, повторённое с приставкой: строка вдвое длиннее, а нового в ней ноль. Показывается путь у патчей из подкаталога — и тогда вторая строка сама работает сигналом «этот лежит не там, где все», — и когда запрос поиска попал в путь, но не в имя: скрыть строку, из-за которой патч оказался в выдаче, значило бы соврать, почему он тут.

В раскрытии строки, в блоке koji, теги разведены по двум строкам. «Основной тег» — тот, через который билд попал в этот снапшот: набран жирным цветом текста и подписан (прямой) или (унаследован). «Другие теги» — где тот же билд висит ещё; они приглушены, а когда таких тегов нет, строка остаётся на месте с прочерком, чтобы блоки соседних раскрытых строк не разъезжались. Поиск ищет и по другим тегам, так что запрос вида os-9.2-candidate найдёт все билды, стоящие в этом теге.

В колонке «патчи» рядом с числом стоит полоска состава: цвет занимает долю, равную доле своего класса среди патчей этого билда. Длина полоски одна у всех строк — иначе доли нельзя сравнивать между билдами, а «сколько патчей» и так сказано числом слева. Класс, представленный одним патчем из сотни, всё равно виден: у сегмента есть пол в 2 пикселя, а точные числа показывает подсказка при наведении.

Колонка «собран» показывает дату и время в московском времени в два уровня: дата первой строкой, время второй и бледнее. Колонку читают по датам, секунды нужны, когда до строки уже дошли, а одной строкой она держала под собой девятнадцать знаков ширины — те, которых не хватало имени компонента слева: оно переносилось, и «NetworkManager» разрывался посередине. Теперь имя не переносится вовсе. В раскрытии то же значение стоит целиком, с пометкой МСК. Хаб отдаёт UTC, и в снапшоте хранится именно он — перевод делает сама страница (toMsk в assets/js/viewmodel.js), так что один и тот же снапшот всегда даёт один и тот же вид. Москва — UTC+3 круглый год, перехода на летнее время в России нет с 2014, поэтому смещение задано числом (MSK_SHIFT_MS) и считается через Date.UTC, а не через базу зон и не через местный пояс: иначе дашборд показывал бы разное время на разных машинах. Понадобится другая зона — меняется эта одна константа. Дата без часа (снапшот прежней версии) не переводится: прибавив три часа к неизвестному времени, дашборд утверждал бы то, чего не знает.

В колонке «владелец» стоит koji-логин того, кто запустил билд. Сортировка по ней собирает билды одного человека подряд.

Раскрытие — полная карточка билда, а не выжимка из того, чего нет в строке: в блоке koji стоят NVR, теги, время сборки, владелец, идентификаторы билда и задачи и ссылка на билд; в блоке gitlab — проект, ветка, наличие каталога PATCH и ссылка. Поле, которое видно и в строке, из карточки не выкидывается: сюда приходят, когда строки уже мало, и заставлять читать в двух местах сразу незачем. Исключение одно — метки: у них своя колонка, и второй такой же полосы в раскрытии нет.

Строка с веткой подписана по виду источника: «ветка», когда билд собран с ветки, «коммит», когда с коммита, и «srpm», когда его собрали не из git, а из готового SRPM. Значение в ней тогда не имя ветки, а хеш или имя файла, и подписать его веткой значило бы соврать. У сборки из SRPM и сам блок зовётся srpm, а не gitlab: GitLab в ней не участвовал вовсе, и пустые «проект» и «ссылка» в нём — не недосмотр. То же в раскрытии «Изменений»: подпись у каждой стороны своя, и пара «ветка → srpm» читается как есть.

Вкладка «Изменения»

Появляется, только когда на странице два и более снапшота. Сравнить можно любые два загруженных снапшота, а не только соседние по цепочке. Выбор — двумя кликами по узлам рельса, того же ряда снапшотов, что стоит над вкладками: первый клик отмечает узел кольцом, второй задаёт диапазон. Клик по отмеченному узлу снимает отметку. Пока отметка стоит, таблица не трогается — начатый выбор ни на что не влияет, пока не завершён вторым кликом.

Концы диапазона на рельсе залиты, а узлы между ними обведены вполсилы: белым — то, что сравнивается, серым — то, что стоит в стороне, и промежуточным — снапшоты, которые в сравнение не попали, но лежат в его сроке. Диапазон os-9.1 → os-9.5 не знает, что было в os-9.2, os-9.3 и os-9.4, и рельс об этом говорит.

Направление задаёт цепочка, а не порядок кликов: рельс упорядочен слева направо от старого тега к новому, и «было» — то, что левее. Кликнули третий узел, потом первый — получите переход от первого к третьему. Обратный порядок молча поменял бы местами «появился» и «исчез». Пока ничего не отмечено, открыт самый широкий диапазон — вся цепочка.

Переходы между соседними тегами и сводный по всей цепочке посчитаны заранее, при загрузке; любой другой диапазон считается в момент выбора. Заметной паузы это не даёт: это один дифф, столько же работы, сколько страница уже делает на загрузке для каждого соседнего перехода.

Диапазон можно назвать и ссылкой: pair= в адресе называет любые два конца полными именами прогонов. Короткая форма по тегам, pair=os-9.1..os-9.2, по-прежнему читается — она выбирает последний диапазон, у которого теги стоят в написанном порядке: самый свежий левый конец, у которого правый ещё есть где-то правее, а у него самого — самый свежий из подходящих правых. Если в написанном порядке ссылка не читается вовсе, порядок разворачивается по цепочке.

Первым рядом стоят четыре большие карточки — итоги перехода, тот же ряд, что на «Состоянии» занимают итоги тега: было и стало — сколько билдов в теге на каждом конце, разница — на сколько их стало больше или меньше, срок — сколько времени прошло между сборами.

Числа сторон берутся у самих снапшотов, а не по строкам таблицы: строка — это компонент перехода, и компонент, которого на этой стороне ещё или уже нет, в ней всё равно стоит. Под числом стороны — тег и время сбора: тега мало, два прогона одного тега законны, и «было os-9.4 → стало os-9.4» без времени сбора не сказало бы, какой из них какой. Ноль в разнице не значит «ничего не менялось»: сколько компонентов ушло, столько могло и прийти, — поэтому под ней стоят появившиеся и исчезнувшие по отдельности. Срок считается той же мерой, что подписывает отрезок рельса.

Фильтра у этих четырёх нет: итог перехода — не срез таблицы, а то, между чем считали, и кнопка обещала бы клик, которому нечего делать.

Ряды карточек занимают строку целиком, и последняя строка тоже. Браузер сам этого не делает: он набивает строку под завязку и про остаток не думает, поэтому одиннадцать срезов при десяти влезающих давали строку из одной карточки и пустоту за ней. Страница считает, на сколько строк карточки делятся поровну — одиннадцать по шесть это шесть и пять, — и задаёт ширину сама; на узком окне тем же правилом делятся и четыре больших итога, ложась два и два вместо трёх и одного. Пересчитывается это при смене снапшота, вкладки и размера окна.

Ниже — одиннадцать карточек-счётчиков диффа: агрегат «изменились» (версия, патчи, состав RPM, ветка или тег — что угодно из перечисленного ниже), «появились» (added), «исчезли» (removed), «версия выросла» (upgraded), «версия упала» (downgraded), «версия та же» (unchanged — но патчи или RPM могли измениться и тогда), «патчи пришли» (patches+), «патчи ушли» (patches-), «состав RPM» (repackaged), «сменили ветку» (branch-changed), «переехали между тегами» (tag-changed). Ниже — таблица изменившихся компонентов с раскрытием «было / стало».

tag-changed ставится только тому билду, который сам остался прежним (совпал NVR), а koji-тег у него поменялся: билд вытащили из родительского тега и затеговали напрямую или наоборот. Обновление компонента такой метки не получает — новый билд лежит в новом теге по определению, и про него уже всё сказано меткой upgraded. Сравнивается сам тег билда, а не то, выглядит ли он прямым из выбранного: иначе при os-9.2, наследующем os-9.1, переехавшим оказался бы каждый нетронутый билд.

«Было» — это состояние, а не половина диффа. Слева стоит то, что было на тот момент: списки патчей и пакетов без единой пометки. Там ничего не происходило, и вычеркнутая строка утверждала бы, будто происходило.

Весь переход показан справа, в «стало». Уцелевшее набрано обычным, ушедшее зачёркнуто и помечено на своём прежнем месте среди уцелевших, пришедшее помечено + и стоит внизу своей группы. Так одна колонка отвечает и на «что теперь», и на «что с этим стало», а вторая остаётся точкой отсчёта.

Класс патчей или архитектура, откуда ушло всё, остаётся в «стало» с нулём в счётчике и одной зачёркнутой строкой: «был и кончился» — тоже ответ. Счётчик блока везде считает новое состояние, зачёркнутое в него не входит.

Списки RPM разбиты на блоки по архитектуре (src, noarch, дальше остальные по алфавиту). Подпакет сопоставляется по name.arch, без version-release, — иначе обновление билда выглядело бы как полная замена состава. Именно это сопоставление и решает, что зачеркнуть, а что дописать внизу блока.

Сводка стороны — такая же полная карточка билда, как в «Состоянии»: версия, тег, время сборки, владелец, ветка, проект и ссылки на билд и на исходник. «Было» и «стало» — это две карточки одного компонента, снятые в разные моменты, и уходить из раскрытия за остальным человеку негде. Изменившееся помечено на стороне «стало» — тег, ветка, владелец, проект. Время сборки не помечается никогда: у пересобранного компонента оно разное всегда, и пометка на нём ничего не сообщала бы. У смены владельца и переезда проекта своей метки нет и фильтра по ним тоже: это подробность, которую видно, только когда строку уже раскрыли.

Раскрытие собрано парами: шапка к шапке, сводка к сводке, патчи к патчам, пакеты к пакетам — каждая пара стоит в своей строке сетки и потому имеет общую высоту. Иначе списки пакетов начинались бы на разной высоте: патчей слева три, справа пять — и правый уезжал бы вниз на два ряда.

Метки строк

Вычисляемые метки поверх строки, к koji-тегам отношения не имеют. Стоят в колонке «метки» — не «теги»: тег в этой таблице один, koji-тег билда, и он в своей колонке слева.

На вкладке «Состояние»: autogen, cve, sast, dast, coverage, spec, changelog, files, other (класс патча, присутствует в билде хотя бы один патч этого класса), inherited (билд висит не в выбранном теге, а в одном из его родителей), no-patch (у ветки нет каталога PATCH, т.е. patch_dir_present == false), no-source (в билде нет extra.source.original_url), from-commit (билд собран не с ветки, а прямо с коммита — источник это допускает, но диффа веток тогда не будет), from-srpm (билд собран не из git, а из готового SRPM: ветки у него нет, каталог PATCH читать негде, и патчей в строке не будет — это не поломка, а другой способ собрать), gitlab-error (репозиторий, ветка или каталог патчей недоступны — включает и bad source url, и любой gitlab: ..., включая 401/403 без токена), internal-error (сбор данных по билду упал с неожиданной исключительной ситуацией — сама ошибка не роняет весь прогон, а записывается в problems билда).

На вкладке «Изменения»: added, removed, unchanged, upgraded, downgraded (статус компонента) и, если применимо, patches+, patches-, repackaged (изменился список RPM без смены EVR), branch-changed, tag-changed.

Фильтры, поиск, ссылка на срез

У каждого признака три положения: неважно, есть и нет. Ставят их в меню под кнопкой «Фильтры» — она же показывает, сколько условий стоит сейчас, и горит, пока хоть одно стоит. Клик по карточке-счётчику или по метке строки по-прежнему ставит и снимает фильтр, только двумя положениями из трёх: «есть» и «неважно». Отрицание ставят в меню; карточка его показывает — приглушается и обводится цветом убранного, — а клик по ней снимает.

Признаки разложены по группам: на «Состоянии» это классы патчей, свойства билда и проблемы, на «Изменениях» — статус и что изменилось. У каждой группы переключатель все / любой из. «Все» — умолчание и то же самое, что было раньше: складывать по И. «Любой из» складывает по ИЛИ отмеченное как «есть» внутри одной группы; отмеченное как «нет» остаётся запретом при любом положении переключателя — «нет autogen» значит «точно не autogen», и складывать такое по ИЛИ незачем. Группы между собой всегда по И.

Так задаётся запрос вроде «есть CVE-патч и при этом нет autogen-патча»: два клика в группе классов. Рядом с каждым признаком стоит число — сколько строк подходит под него самого, без оглядки на остальные фильтры.

Поле поиска ищет по имени компонента, NVR, koji-тегу билда, ветке, именам файлов патчей, CVE-ID и именам RPM. Текущий вид — активная вкладка, выбранный снапшот («Состояние») или переход («Изменения»), фильтры, поисковый запрос, сортировка — синхронизируется с location.hash.

Снапшот стоит в хеше именем — тегом и временем сбора, — а переход именами обоих концов: номер после перестановки или удаления показал бы другой снапшот, ничем не выдав подмены, а одного тега мало, когда на странице два прогона одного тега. Имя уезжает в адрес экранированным, так что глазами человек видит tag=os-9.2%402026-08-01T00%3A00%3A00%2B03%3A00, а не os-9.2@2026-08-01T00:00:00+03:00; читать и править удобнее короткую форму tag=os-9.2 — она тоже принимается. Двойников короткая форма не различает и выбирает последний подходящий снапшот по цепочке: при обычном порядке это самый свежий, а после ручной перестановки — тот, кого человек поставил последним. Так же читается и короткая форма перехода, os-9.1..os-9.2.

Фильтр из хеша, который на живых данных не опознаётся, молча выбрасывается: иначе страница показывала бы пустую таблицу под фильтр, которого нет ни на одной карточке — его нечем было бы снять. Проверка идёт по трём спискам: постоянные подписи самой страницы, классы патчей загруженных снапшотов и метки строк того снапшота или перехода, который сейчас показан. Постоянные подписи — это в том числе статусы диффа, и они проходят проверку всегда, независимо от вкладки: #tab=state&f=downgraded разбор переживёт и даст на «Состоянии» пустую таблицу. Довод это не рушит — такой фильтр виден в меню и снимается оттуда, — но правило именно такое.

Фильтры в ссылке: f=cve,-autogen — минус перед ключом значит «нет», any=classes — список групп, переключённых в «любой из». Ссылка без минусов и без any= открывает тот же срез, что и до появления трёх положений.

Ссылку на срез можно скопировать и переслать, но данных она не несёт: тот, кто её откроет, сперва подгружает те же снапшоты, и только тогда вид восстановится. Это прямое следствие того, что страница и данные разделены.

Логи

Всё логирование настраивается одним общим флагом --log-level (error/warning/info/debug, по умолчанию info) и идёт в stderr:

Уровень Что в нём видно
error только фатальные ошибки — та самая одна строка перед кодом возврата 2
warning плюс проблемы отдельных билдов (то, что попадает в problems снапшота и метка gitlab-error/internal-error в дашборде), повторы запросов к GitLab после 429/5xx, отсутствие multicall на старом хабе koji (сбор идёт последовательными вызовами вместо пакетных), превышение --max-problems
info (по умолчанию) плюс имя записанного файла (у обеих подкоманд), а у collect — ещё и параметры прогона (хаб, теги, --jobs, задан ли токен), размер тега, прогресс сбора примерно на каждые 5% билдов, итоговая сводка по тегу, общее время
debug плюс каждый запрос к GitLab (URL, код ответа, длительность, тело ответа при ошибке) и каждый вызов koji (метод, число билдов в пакете, длительность — у XML-RPC своих URL и кода ответа нет), попадания в кэш дерева патчей, размер собранной страницы, полный traceback фатальной ошибки

Пример строки в обычном формате (info и выше):

23:08:22 INFO    cli: написан dashboard.html

На debug формат тот же, но с именем потока — при --jobs больше единицы сбор идёт в пуле потоков, и без имени в строке не разобрать, какой запрос к какому потоку относится:

23:08:53 DEBUG   [w_0] gitlab: GET https://gitlab.example.com/api/v4/projects/g%2Fpkg1/repository/tree ref=br path=PATCH → 200 за 0.00 с

Логи всегда идут в stderr, а не в stdout, поэтому -o (файл снапшота или страницы) и 2>run.log (файл журнала) друг другу не мешают:

python3 -m dashboard --config dashboard.yaml --log-level debug collect \
    --tag os-9.2 -o os-9.2.json 2>run.log

--log-level debug на теге в несколько сотен билдов даёт тысячи строк — это ожидаемо, уровень для разбора конкретной проблемы (например, почему билд получил gitlab-error), а не для повседневного запуска.

Токен GitLab (см. раздел выше) ни на одном уровне, включая debug, в лог не попадает: в строку параметров прогона пишется только задан/не задан, а из параметров HTTP-запроса в лог уходят ref/path/page, но не заголовки.

Коды возврата

Код Когда
0 успех
1 только для collect: суммарно по всем собранным в этом прогоне снапшотам проблемных билдов больше, чем --max-problems
2 фатальная ошибка: конфиг не читается или не проходит валидацию, koji недоступен на уровне транспорта, шаблон страницы или скрипт к ней не читается, ошибка ввода-вывода и т. п.

--max-problems без значения (флаг не передан) отключает проверку и код 1 не возвращается никогда. Считаются проблемные билды только что собранных снапшотов, поэтому проверка возможна лишь у collect; page возвращает 0 сразу после успешной записи файла — снапшотов он не видел и считать ему нечего.

Разбор снапшотов кодом возврата больше не отражается вовсе: их читает страница, а не CLI, и негодный файл она называет на экране, рядом с зоной загрузки (см. «Как в него попадают снапшоты»).

Во всех случаях кода 2 пользователю на уровне error печатается одна строка вида <описание>: <исключение> — конфиг ли не читается, не собирается ли страница, ошибка ли это ввода-вывода, или что-то совсем непредвиденное (koji недоступен на уровне транспорта и т. п.). Полный traceback этой же ошибки печатается дополнительно, но только на --log-level debug — см. «Логи».

Формат снапшота

Файл, который пишет collect и читает страница дашборда, — JSON-массив снапшотов (или один объект снапшота — обе формы понимает и страница, и питоновская модель). Пример на одном билде:

[
 {
  "schema": 1,
  "dashboard": "1.0.0",
  "tag": "os-9.1",
  "generated": "2026-07-01T00:00:00+03:00",
  "koji_hub": "https://hub/kojihub",
  "koji_web": "https://hub/koji",
  "patch_classes": ["AUTOGEN", "CVE", "SAST", "DAST", "COVERAGE",
                    "DISTSUFFIX", "LICENSE", "SPEC", "CHANGELOG", "FILES",
                    "other"],
  "builds": [
   {
    "nvr": "nginx-1.24.0-1.el9",
    "name": "nginx",
    "version": "1.24.0",
    "release": "1.el9",
    "epoch": null,
    "build_id": 1,
    "task_id": 2,
    "owner": "builder",
    "completed": "2026-05-14 10:00:00",
    "tag_name": "os-9-base",
    "tags": ["os-9-base", "os-9.1"],
    "source": {
     "raw": "git+ssh://git@h/g/nginx?#origin/main",
     "host": "h",
     "project": "g/nginx",
     "ref": "main",
     "ref_kind": "branch",
     "web_url": "https://gl/tree"
    },
    "patch_dir_present": true,
    "patches": [
     {
      "path": "PATCH/CVE-2024-7347.patch",
      "name": "CVE-2024-7347.patch",
      "class": "CVE",
      "cves": ["CVE-2024-7347"],
      "web_url": "https://gl/blob/CVE-2024-7347.patch"
     }
    ],
    "rpms": ["nginx-1.24.0-1.el9.x86_64"],
    "problems": []
   }
  ]
 }
]

schema — версия формата (сейчас 1); файл с другим значением страница не примет и скажет об этом прямо, назвав чужую версию, а не «это не снапшот»: такой файл сделан другой версией dashboard, и человеку полезнее знать какой.

dashboard — версия инструмента, записавшего файл. Поле необязательное: снапшоты, собранные до его появления, читаются как прежде, и схему оно не меняет — версия формата и версия инструмента растут порознь. Отвечает оно на вопрос «чем это собрано», который возникает, когда файл прислали со стороны. Та же версия стоит в собранной странице — в <meta name="generator"> и рядом с заголовком, — и печатается по --version. Что менялось от версии к версии, сказано в CHANGELOG.md.

generated — время сбора этого снапшота, в местной зоне машины, где шёл collect. Оно же — половина имени снапшота: пара «tag и generated» отличает два прогона одного тега друг от друга, по ней страница выстраивает цепочку, ловит повторную загрузку и называет снапшот в адресной строке.

patch_classes — имена классов патчей в порядке правил классификатора. Дашборду этот порядок нужен для карточек классов, меток строк и разбора фильтров из ссылки, а взять его больше неоткуда: конфига у страницы нет. Когда снапшотов несколько, список задаёт первый из них, а остальные могут только дописать в конец то, чего в нём не было.

Поле необязательное: снапшоты, собранные до его появления, читаются по-прежнему. Но выведет классы из самих патчей (по алфавиту) страница только тогда, когда поля нет ни у одного загруженного снапшота — иначе список берётся у тех, кто его несёт, и всё. Смешанный набор — старый снапшот без поля рядом с новым — это ровно тот случай из «Быстрого старта», где тег сравнивают с ним же месяц назад: список страницы будет списком нового снапшота, а класс, который встречается только в старом, в него не попадёт. Совсем такой класс не пропадает: пока выбран старый снапшот, у него есть карточка — хвостом, за перечисленными, — и метку в строке он тоже получит. Но в списке классов страницы его не будет, а фильтр по нему из присланной ссылки опознается только там, где такие строки видны: единственной опорой остаются метки строк выбранного снапшота. Собрать оба снапшота одной версией dashboard дешевле, чем разбираться в такой странице.

completed — время окончания сборки в виде YYYY-MM-DD HH:MM:SS, в UTC, как его отдаёт хаб: снапшот хранит то, что сказал koji, а в московское время значение переводится уже на странице. Доли секунды и смещение зоны срезаются. Снапшоты, собранные до появления времени, несут одну дату — дашборд с ними работает как прежде, просто без часов.

tag_name — koji-тег, в котором билд действительно затегован (его отдаёт listTagged); совпал с tag снапшота — билд прямой, не совпал — унаследован оттуда. Поле необязательное: снапшоты, собранные до его появления, читаются по-прежнему, и такие билды показываются как «тег неизвестен», а не как прямые. tags — все koji-теги билда (их отдаёт listTags), включая tag_name; в дашборде они показаны в раскрытии строки, а tag_name среди них выделен. Поле тоже необязательное: пустой список означает «не спрашивали», и дашборд тогда показывает только то, что знает из listTagged.

koji_hub/koji_web снапшота — не текущий конфиг, а то, чем реально пользовался collect в момент сбора этого конкретного снапшота. Страница берёт koji_web у того снапшота, из которого пришёл показанный билд: на «Состоянии» — у снапшота своей строки, на «Изменениях» — у той стороны пары, откуда взят билд. Так ссылки не уводят на чужой хаб, когда рядом легли снапшоты разных прогонов. Сторона пары ищется по имени тега, поэтому у двух прогонов одного тега адрес возьмётся у первого из них — заметно это, только если у прогонов разный koji_web. Разные koji_hub страница ловит предупреждением, koji_web не сравнивается.

source — откуда билд собран, разобранный extra.source.original_url. Вид источника говорит ref_kind: branch — из ветки, commit — прямо с коммита (в ref тогда хеш), srpm — не из git, а из готового SRPM (в ref имя файла, host и project пусты, ходить в GitLab не за чем), none — ссылка разобрана, но ветки в ней нет. Само поле необязательное: у билда без original_url его нет вовсе, и в строке стоит метка no-source.

patch_dir_present трёхзначен:

  • true — каталог PATCH в ветке прочитан (список файлов, пусть и пустой, получен);
  • false — ветка существует, но каталога PATCH в ней нет;
  • null — не определялся или определить не удалось: у билда вовсе нет источника, ссылка на него не разбирается, GitLab ответил ошибкой — или билд собран из готового SRPM, и каталог читать негде. В первых трёх случаях причина всегда есть в problems; в последнем problems пуст, а вид источника говорит ref_kind: "srpm" и метка from-srpm в строке.

false и null в этом случае клиент различает вторым запросом. Ответ 404 на запрос дерева PATCH сам по себе неоднозначен: так отвечает и «в ветке нет каталога PATCH», и «ветки уже нет», причём формулировка зависит от версии GitLab — встречаются 404 Tree Not Found, 404 invalid revision or path Not Found, причина в поле error вместо message и вовсе пустое тело. Поэтому решает не текст ответа: любой 404, кроме явного 404 Project Not Found, уточняется запросом GET /repository/commits/<ref>. Если коммит найден — ветка существует, каталога нет, patch_dir_present = false и никакой проблемы не записывается. Если и там 404problems: ["gitlab: ref not found"] и patch_dir_present = null, а не молчаливое «патчей нет». 404 Project Not Found распознаётся сразу и второго запроса не вызывает.

problems — список текстовых причин, с которыми накопитель столкнулся по конкретному билду; префиксы значимы и определяют теги в дашборде (см. выше): no source url, bad source url: ..., gitlab: ... (в том числе gitlab: no ref in source url, gitlab: unknown host, gitlab: ref not found, gitlab: 401 .../gitlab: 403 ... без валидного токена), internal error: ....

Устройство проекта

Питон собирает данные, страница их показывает — по этой границе разложены и файлы.

Что Где
разбор командной строки dashboard/cli.py
конфиг, классификатор, модель снапшота, логи config.py, classify.py, model.py, logs.py
сбор данных collect.py, kojiclient.py, gitlabclient.py, httpclient.py, sourceurl.py
сборка страницы build.py + assets/dashboard.html + assets/css/*.css + assets/js/*.js

httpclient.py держит сам разговор по HTTP — повторы, паузы, Retry-After и вычистку токена из сообщений об ошибках. gitlabclient.py знает только про GitLab: как спросить дерево ветки, как отличить «каталога нет» от «сервер не ответил» и как собрать веб-ссылку.

build.py не считает ничего: он читает шаблон, подставляет вместо двух плейсхолдеров стили и скрипты и отдаёт один файл.

Стили

Восемь файлов в assets/css/, каждый про свой участок страницы. Порядок важен: при равной специфичности выигрывает то, что ниже, поэтому он задан в STYLES и сторожится тестом.

Файл За что отвечает
base.css переменные, сброс, типографика, палитра классов патчей
layout.css шапка, панель источников, экран загрузки, вкладки, панель управления, кнопка «наверх»
rail.css рельс цепочки: узлы, отрезки, крестики, призрак
cards.css карточки-счётчики
table.css таблицы обеих вкладок и всё, что раскрывается под строкой
filters.css кнопка фильтров, плашка меню, тройной переключатель
toasts.css стопка всплывающих сообщений в правом нижнем углу
tip.css всплывающая подсказка

Скрипты

Восемнадцать файлов в assets/js/. Порядок в SCRIPTS задан по зависимостям, а не по алфавиту: каждый следующий рассчитывает, что предыдущие уже положили себя в KP.

Разложены они по тому, чем модуль владеет, а не по тому, к какой вкладке относится.

Чистые — данные приходят доводами, наружу уходят строки. Ни DOM, ни состояния страницы они не видят и проверяются без заглушки браузера.

Файл За что отвечает
vercmp.js сравнение версий по правилам rpm
rpms.js архитектура пакета и порядок RPM
diff.js сравнение снапшотов и цепочка пар
viewmodel.js данные страницы: строки, счётчики, метки, перевод времени
text.js экранирование, подсветка запроса, склонение, время
labels.js как страница называет ключи из данных
hash.js разбор и сборка строки адреса
markup.js куски разметки, общие обеим таблицам
tables.js строки и детали таблиц
cards.js карточки-счётчики

Владельцы участков DOM — у каждого свой узел и свои обработчики, навешенные один раз.

Файл За что отвечает
filters.js кнопка фильтров и меню под ней
rail.js рельс: разметка, выбор снапшота и диапазона, перестановка
files.js загрузка снапшотов файлами
tips.js подсказки
toasts.js всплывающие сообщения: окошки в углу, свои таймеры

Состояние и корень.

Файл За что отвечает
store.js загруженные снапшоты: разбор, порядок, дубликаты, откат
page.js состояние страницы и всё, что из него считается
ui.js находит узлы, кладёт в них разметку, разводит события

page.js заводится фабрикой, а не живёт синглтоном: тест поднимает свежее состояние одним вызовом. ui.js — единственный, кто знает и про DOM, и про всех остальных; перерисовку он раздаёт владельцам участков объектом app.

Раньше всё это считал питон и запекал результат внутрь HTML. Так можно, пока набор снапшотов известен в момент сборки страницы, — но человек складывает файлы рядом уже после того, как страница написана, и дифф между двумя произвольно подгруженными снапшотами взять готовым неоткуда. Считать его может только страница, и раз уж вычисления всё равно оказались в браузере, держать вторую их копию в питоне было бы обещанием, которое некому проверить. Поэтому render.py, diff.py, rpms.py и rpmvercmp.py из проекта ушли, а с ними и подкоманды render и run.

Разработка

Ветки

Гитфлоу в облегчённом виде: разработчик один, и ветка на стабилизацию релиза простаивала бы пустой.

Ветка Откуда Куда вливается Что в ней
master то, что выпущено; каждый её коммит помечен тегом vX.Y.Z
develop master master при релизе то, что готово, но ещё не выпущено
feature/* develop develop одна задача
hotfix/* master master и develop срочная починка выпущенного

Задача живёт в своей feature/* и вливается в develop слиянием с --no-ff: отдельный коммит слияния оставляет в истории видимую границу задачи, а перемотка её потеряла бы.

Релиз — слияние develop в master и тег vX.Y.Z на нём. Номер к этому моменту уже стоит в dashboard/__init__.py: его поднимают вместе с самим изменением (см. ниже), а тег лишь отмечает, что именно этот номер вышел. hotfix/* идёт от master, поднимает младший номер и вливается в обе ветки — иначе починка потеряется в следующем релизе.

Версия и запись о ней

Номер живёт в dashboard/__init__.py и поднимается тем же коммитом, что и само изменение, — не отдельным «релизным». Сломали флаг CLI или формат снапшота — старший номер, появилась возможность — средний, починили или поправили вид — младший; правки только в тестах, документации или комментариях номер не двигают. Вместе с номером в CHANGELOG.md заводится запись о том, что увидит человек, который этим пользуется. Что запись есть, сторожит tests/test_version.py: покрасневший тест означает забытый CHANGELOG, а не сломанный код.

Тесты

Наборов тестов два — по разные стороны той же границы:

python3 -m unittest discover -s tests -v
node --test tests/js/*.test.js

Питоновский набор проверяет сбор данных и CLI, набор для Node — всё, что делает страница; второму нужен Node со встроенным --test (18 и новее). Оба набора без внешних сетевых зависимостей: koji и GitLab в питоновских тестах подменены фейками из tests/fakes.py, а браузерным скриптам хватает заглушки DOM из tests/js/domstub.js — она строит дерево из настоящего assets/dashboard.html, поэтому пропавший в шаблоне id роняет тест, а не молча ломает страницу. Сколько тестов прошло, скажут сами unittest и node --test в последних строках вывода.

Отдельно стоит tests/js/fixtures/page-data.golden.json — эталон данных страницы, порождённый ещё питоновским render.py на фикстурах tests/fixtures/rich-old.json и rich-new.json — только на этих двух, без остальных шести: те появились позже и нужны глазам, а не сверке. С ним значение в значение сверяется buildPageData, и это сторож переноса вычислений в браузер: пока эталон совпадает, страница считает ровно то же, что считал питон. Пересчитать его больше нечем — render.py и tests/test_parity.py удалены вместе с питоновским слоем представления, — поэтому эталон правят руками и только осознанно, когда изменение данных страницы задумано. Тест рядом с ним следит, чтобы в эталоне остались интересные случаи (все статусы диффа, tag-changed, repackaged, branch-changed, билды с неизвестным и с унаследованным тегом): без этой проверки обеднение фикстур прошло бы незаметно и сверка стала бы сверкой ни с чем.

Остальные шесть смотрят глазами, но без присмотра не оставлены: tests/test_fixtures.py проверяет, что каждый файл читается, что в нём остался случай, ради которого он заведён, и что правит их tests/fixtures/make_rich_fixtures.py, а не рука. Генератор запускают из корня репозитория, и он переписывает только те файлы, в которых изменились данные: версию записавшего каждый снапшот несёт своё, и на подъёме номера фикстуры не трогаются.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages